﻿//(c) Alexandru Ghiondea, 2011
using System;
using System.Collections.Generic;
using System.Text;
using JSONDotNET.Syntax;

namespace JSONDotNET
{
    public class Parser
    {
        public static CompilationUnit ParseText(string text)
        {
            return new Parser(new Lexer(text).Lex()).Parse();
        }
        private List<Token> toks;
        public Parser(List<Token> toks)
        {
            this.toks = toks;
        }

        public CompilationUnit Parse()
        {
            if (peekToken().kind != TokenKind.OpenBrace)
            {
                throw new ParserException(string.Format("Expected {0} found {1}", TokenKind.OpenBrace, peekToken().kind), peekToken().pos);
            }
            else
            {
                //we start parsing
                var unit = parseObject();

                // Anything after this point is an error
                if (peekToken() != null)
                {
                    throw new ParserException(string.Format("Object should not have trailing tokens! Found {0} after the {1}", peekToken().kind, TokenKind.CloseBrace), peekToken().pos);
                }

                return new CompilationUnit() { Unit = unit };
            }
        }
        private ParsedObject parseObject()
        {
            if (peekToken().kind != TokenKind.OpenBrace)
                throw new ParserException(string.Format("Object should start with {0} found {1}", TokenKind.OpenBrace, peekToken().kind), peekToken().pos);

            //we eat the { token
            eatToken();

            //we parse the members
            ParsedObject po = new ParsedObject();
            po.Members = parseMembers();

            if (peekToken().kind != TokenKind.CloseBrace)
                throw new ParserException(string.Format("Object should end with {0} found {1}", TokenKind.CloseBrace, peekToken().kind), peekToken().pos);

            //we eat the } token
            eatToken();

            return po;
        }
        private List<ParsedPair> parseMembers()
        {
            //the members are like> string : value, string : value
            var members = new List<ParsedPair>();

            while (peekToken().kind != TokenKind.CloseBrace)
            {
                members.Add(parsePair());

                //do we have a comma
                if (peekToken().kind == TokenKind.Comma)
                    //eat it
                    eatToken();
            }

            return members;
        }
        private ParsedPair parsePair()
        {
            if (peekToken().kind != TokenKind.DoubleQuote)
                throw new ParserException(string.Format("A pair should start with a {0}, found {1}", TokenKind.DoubleQuote, peekToken().kind), peekToken().pos);

            ParsedPair pp = new ParsedPair();
            pp.Name = parseStringConstruct();

            if (peekToken().kind != TokenKind.Colon)
                throw new ParserException(string.Format("The name should be separated from the value by {0}, found {1}", TokenKind.Colon, peekToken().kind), peekToken().pos);

            //we eat the colon
            eatToken();

            //parse the value
            pp.Value = parseValue();

            return pp;
        }
        private ParsedValue parseValue()
        {
            var tok = peekToken();

            if (tok.kind == TokenKind.DoubleQuote) //parse string
                return parseString();
            else if (tok.kind == TokenKind.OpenBrace) //parse object
                return parseObject();
            else if (tok.kind == TokenKind.OpenSquareBrace) //parse array
                return parseArray();
            else if (tok.kind == TokenKind.Digit || tok.kind == TokenKind.Minus || tok.kind == TokenKind.Plus) //parse number 
                return parseNumber();
            else if (tok.kind == TokenKind.False) //parse false literal
                return parseFalseValue();
            else if (tok.kind == TokenKind.True) //parse true literal
                return parseTrueValue();
            else if (tok.kind == TokenKind.Null) //parse null literal
                return parseNullValue();

            throw new ParserException(string.Format("Value not recognized: {0}", tok.kind), tok.pos);
        }
        private ParsedValue parseFalseValue()
        {
            if (peekToken().kind != TokenKind.False)
                throw new ParserException(string.Format("Expected {0}, found {1}", TokenKind.False, peekToken().kind), peekToken().pos);

            eatToken();
            return new FalseValue();
        }
        private ParsedValue parseTrueValue()
        {
            if (peekToken().kind != TokenKind.True)
                throw new ParserException(string.Format("Expected {0}, found {1}", TokenKind.True, peekToken().kind), peekToken().pos);

            eatToken();
            return new TrueValue();
        }
        private ParsedValue parseNullValue()
        {
            if (peekToken().kind != TokenKind.Null)
                throw new ParserException(string.Format("Expected {0}, found {1}", TokenKind.Null, peekToken().kind), peekToken().pos);

            eatToken();
            return new NullValue();
        }
        private StringValue parseString()
        {
            return new StringValue() { Value = parseStringConstruct() };
        }
        private NumberValue parseNumber()
        {
            bool negative = false;
            //if we have a -
            if (peekToken().kind == TokenKind.Minus)
            {
                negative = true;
                eatToken();
            }
            else if (peekToken().kind == TokenKind.Plus) //we have a +
            {
                negative = false;
                eatToken();
            }

            if (peekToken().kind != TokenKind.Digit)
                throw new ParserException(string.Format("Expected {0}, found {1}", TokenKind.Digit, peekToken().kind), peekToken().pos);

            double number = 0;
            Token tok = null;

            double fracPart = 0;
            double exponent = double.NaN;

            while (peekToken().kind == TokenKind.Digit)
            {
                tok = eatToken();
                number = number * 10 + int.Parse(tok.value);
            }

            if (peekToken().kind == TokenKind.Dot)
            {
                //it is like 1.200
                eatToken();

                //we must have a number
                if (peekToken().kind != TokenKind.Digit)
                    throw new ParserException(string.Format("Expected {0}, found {1}", TokenKind.Digit, peekToken().kind), peekToken().pos);

                //we need to build the fraction part
                int count = 0;
                while (peekToken().kind == TokenKind.Digit)
                {
                    tok = eatToken();
                    count++;
                    fracPart = fracPart * 10 + double.Parse(tok.value);
                }

                fracPart = fracPart / Math.Pow(10, count);
                number += fracPart;
            }

            //we might also have an exponent for the number
            if (peekToken().kind == TokenKind.Exponent)
            {
                //after the exponent, we need to read the sign

                eatToken();

                if (peekToken().kind != TokenKind.Minus && peekToken().kind != TokenKind.Plus)
                    throw new ParserException(string.Format("Expected {0} or {1}, found {2}", TokenKind.Plus, TokenKind.Minus, peekToken().kind), peekToken().pos);

                tok = eatToken();
                //now that we know we have an exponent
                exponent = 0;
                bool positive;
                if (tok.kind == TokenKind.Plus)
                    positive = true;
                else
                    positive = false;

                //we now need to read the exponent value
                //we must have a number
                if (peekToken().kind != TokenKind.Digit)
                    throw new ParserException(string.Format("Expected {0}, found {1}", TokenKind.Digit, peekToken().kind), peekToken().pos);

                //we need to build the exponent part
                while (peekToken().kind == TokenKind.Digit)
                {
                    tok = eatToken();
                    exponent = exponent * 10 + double.Parse(tok.value);
                }

                if (positive)
                    number = number * Math.Pow(10, exponent);
                else
                    number = number / Math.Pow(10, exponent);

                //need to build the number from the exponent
            }

            if (fracPart == 0 && double.IsNaN(exponent))
            {
                // The try/catch is here so that we can give a better error message when we have a number that we cannot store
                // as an integer
                try
                {
                    return new IntNumberValue() { Value = checked(negative ? -(long)number : (long)number) };
                }
                catch (OverflowException overflowExc)
                {
                    throw new ParserException("The number could not be represented as an integer number.", peekToken().pos, overflowExc);
                }
            }
            else
                return new DoubleNumberValue() { Value = negative ? -number : number };
        }
        private ParsedValue parseArray()
        {
            if (peekToken().kind != TokenKind.OpenSquareBrace)
                throw new ParserException(string.Format("An array should start with {0}, found {1}", TokenKind.OpenSquareBrace, peekToken().kind), peekToken().pos);

            //we eat the [
            eatToken();
            ArrayValue av = new ArrayValue();

            av.Value = new List<ParsedValue>();

            //an array consists of values separated by commas
            while (peekToken().kind != TokenKind.CloseSquareBrace)
            {
                av.Value.Add(parseValue());
                if (peekToken().kind == TokenKind.Comma)
                    eatToken();
            };

            if (peekToken().kind != TokenKind.CloseSquareBrace)
                throw new ParserException(string.Format("An array should end with {0}, found {1}", TokenKind.CloseSquareBrace, peekToken().kind), peekToken().pos);

            //eat the ]
            eatToken();

            return av;
        }
        private string parseStringConstruct()
        {
            if (peekToken().kind != TokenKind.DoubleQuote)
                throw new ParserException(string.Format("A string should start with {0}, found {1}", TokenKind.DoubleQuote, peekToken().kind), peekToken().pos);

            //we eat the "
            eatToken();

            StringBuilder val = new StringBuilder();
            while ((peekToken()).kind != TokenKind.DoubleQuote)
            {
                var token = eatToken();
                val.Append(token.value);
            }

            if (peekToken().kind != TokenKind.DoubleQuote)
                throw new ParserException(string.Format("An array should end with {0}, found {1}", TokenKind.DoubleQuote, peekToken().kind), peekToken().pos);

            //we eat the "
            eatToken();

            return val.ToString();
        }

        private Token eatToken()
        {
            if (toks.Count > 0)
            {
                var temp = toks[0];
                toks.RemoveAt(0);
                return temp;
            }

            return null;
        }
        private Token peekToken()
        {
            if (toks.Count > 0)
                return toks[0];

            return null;
        }
    }
}
